10장. 부품을 하나씩 조립하고, 사용자 반응을 모은다

출처: 『AI 엔지니어링』(Chip Huyen 지음) | 원서: AI Engineering (O'Reilly) — 본 입문판은 PDF 원문에서 직접 풀어 썼다.

코드는 분위기만 — Python·API·import 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.

지금까지는 모델 하나를 어떻게 잘 다루는지를 봤다.

이 장은 그 조각들을 합쳐 진짜 제품을 만드는 이야기다.

그리고 만든 다음, 사용자가 어떻게 쓰는지 그 반응을 모으는 이야기다.


0. 이 장의 새 단어 (0장에 없는 것만 3개)

이 세 단어만 미리 알면 이 장은 막힘없이 읽힌다.

나머지 어려운 말은 0장 용어집에 이미 있다.


가드레일(guardrail)

한 문장 뜻 — 위험한 입력이나 위험한 출력을 막아 주는 안전 울타리.

일상비유 — 도로 갓길의 가드레일. 차가 낭떠러지로 떨어지기 직전에 막아 준다. 운전은 그대로 하되, 사고만 막는다.

한 줄 예 —

# 들어온 질문에 전화번호가 있으면 가린다
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if has_phone_number(query):
    query = mask(query)  # "010-..." → "[전화번호]"

캐싱(caching)

한 문장 뜻 — 한 번 만든 답을 저장해 두고, 같은 질문이 또 오면 다시 만들지 않고 꺼내 쓰는 것.

일상비유 — 자주 쓰는 답을 적어 둔 포스트잇. 손님이 같은 걸 물으면 매번 계산하지 않고 붙여 둔 쪽지를 그냥 읽어 준다.

한 줄 예 —

# 저장해 둔 답이 있으면 그걸 쓰고, 없으면 새로 만들어 저장
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if query in cache:
    # 계산하거나 처리한 결과를 이 함수를 부른 쪽으로 돌려줍니다.
    return cache[query]

피드백 루프(feedback loop)

한 문장 뜻 — 사용자 반응이 모델을 바꾸고, 바뀐 모델이 다시 사용자 반응을 부르는 돌고 도는 순환.

일상비유 — 마이크와 스피커가 가까울 때 나는 삐- 소리. 스피커 소리가 마이크로 다시 들어가 점점 커진다. 잘 쓰면 선순환, 잘못 두면 악순환이다.

한 줄 예 —

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 사용자 반응 → 모델 개선 → 다시 사용자 반응
feedback = collect(user)        # 반응을 모으고
model = improve(model, feedback)  # 모델을 고치면
# 다음 사용자 반응에 또 영향을 준다 (계속 돌아감)

(귀납 도입) 이런 적 있죠?

"AI 챗봇을 만들었다. 데모는 멋지게 돌았다."

"그런데 사용자에게 풀었더니 난리가 났다."

누구는 회사 기밀을 그대로 챗봇에 복사해 붙였고,

누구는 챗봇이 거짓 답을 사실처럼 말했다고 화를 내고,

누구는 같은 질문을 백 번 해서 요금 폭탄이 터졌다.

여기서 깨닫는다.

모델을 잘 부르는 것과, 믿고 쓸 제품을 만드는 것은 다른 일이다.

제품은 모델 하나로 끝나지 않는다.

입구에 안전 울타리(가드레일)를 세우고,

자주 묻는 건 저장(캐싱)해 두고,

질문 종류에 따라 다른 곳으로 보내고,

만든 뒤엔 사용자가 어떻게 쓰는지(피드백) 지켜봐야 한다.

저자의 방법은 단순하다.

가장 단순한 모양에서 시작해, 필요할 때마다 부품을 하나씩 붙인다.

처음부터 완벽한 설계도를 그리려 하면 복잡함에 깔린다.

# 가장 단순한 시작점 — 질문을 받아 모델에 넘기고 답을 돌려준다
# `answer`에 중간 결과를 담아 다음 줄에서 재사용합니다.
answer = model.ask(query)
# 여기엔 안전장치도, 저장도, 라우팅도 없다. 이제 하나씩 붙인다.

이 장에서 딱 4가지만

  1. 부품을 하나씩 붙인다 — 단순 모델 호출 → 컨텍스트 보강 → 가드레일 → 라우터/게이트웨이 → 캐싱 → 에이전트. 순서대로 살을 붙인다.
  2. 가드레일은 입구와 출구 양쪽에 — 들어올 때 개인정보·공격을 막고, 나갈 때 거짓·유해 답을 막는다.
  3. 만든 뒤엔 들여다볼 수 있게(관찰 가능성) — 로그·지표를 처음부터 심어 둔다. 나중에 붙이면 늦다.
  4. 사용자 반응이 가장 귀한 재료 — 단, 반응에는 편향이 섞여 있으니 곧이곧대로 믿지 않는다.

1. 부품을 하나씩 붙인다 — 점진적 조립

망가지는 장면

"처음부터 완벽한 AI 시스템을 그려 보려 했더니, 박스가 스무 개라 시작도 못 하겠다."

일상비유

레고 집 짓기.

기초 한 칸부터 놓는다. 그다음 벽, 그다음 지붕. 한 번에 완성 집을 쏟아붓지 않는다.

필요 없는 방은 안 만든다. 모든 집에 수영장이 필요한 건 아니니까.

비유 코드 위험
기초 한 칸(단순 호출) answer = model.ask(query) 안전장치가 하나도 없음
벽·지붕 한 장씩(부품 추가) answer = guard(route(cache(model.ask(query)))) 부품이 늘수록 복잡해지고 새 고장점이 생김

한 문장 정의 — 점진적 조립은 가장 단순한 모델 호출에서 시작해, 필요할 때마다 부품(컨텍스트 보강·가드레일·라우터·캐싱·에이전트)을 하나씩 붙여 나가는 설계 방식이다.

붙이는 순서 (저자가 권하는 다섯 단계)

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 1단계: 컨텍스트 보강 — 모델에게 줄 정보를 풍부하게
# 2단계: 가드레일 — 위험을 막는 안전 울타리
# 3단계: 라우터/게이트웨이 — 질문을 알맞은 곳으로, 모델을 한 창구로
# 4단계: 캐싱 — 자주 쓰는 답을 저장해 빠르고 싸게
# 5단계: 에이전트 패턴 — 답이 부족하면 다시 찾아보는 반복 구조

예시 폭격

(예시 1 — 완성예) 작은 메모 앱에 AI 요약을 붙인다. 사용자가 적고, 위험도 낮다. 그냥 단순 호출 하나로 끝낸다. 부품을 더 붙일 이유가 없다.

(예시 2 — 부분완성) 고객 챗봇을 만든다. 회사 기밀이 새면 큰일이다. 빈칸을 채워 보자.

# 위험이 큰 챗봇 → 어떤 부품을 먼저 붙일까?
# `answer`에 중간 결과를 담아 다음 줄에서 재사용합니다.
answer = model.ask(query)
# 정답: ____ 단계(안전 울타리)를 가장 먼저 붙인다

(답: 2단계 가드레일. 기밀 유출 위험이 가장 크니까.)

(예시 3 — 독립적용) 사진을 자동 정리하는 앱을 맡았다. 답이 느려 사용자가 답답해한다. 다섯 단계 중 무엇을 붙이면 좋을까?

(답: 4단계 캐싱. 같은 사진 요약을 또 만들지 않고 저장해 두면 빨라진다.)

주의 — 부품을 붙일 때마다 시스템은 강해지지만 복잡해진다. 그만큼 새로운 고장 종류도 생긴다. 모든 부품이 모든 앱에 필요한 건 아니다.


2. 컨텍스트 보강 — 답하기 전에 자료부터 붙여 준다

망가지는 장면

"모델한테 '우리 회사 환불 규정 알려줘'라고 했더니, 알지도 못하면서 그럴듯한 거짓을 지어낸다."

일상비유

오픈북 시험.

외운 것만으로 답하면 틀린다. 자료를 펴 놓고 찾아보며 답하면 정확해진다.

모델에게도 답하기 전에 관련 자료를 찾아 손에 쥐여 주는 것이다. 이게 0장에서 본 RAG다.

비유 코드 위험
맨몸으로 시험(자료 없음) model.ask("환불 규정?") 모르는 걸 지어냄(환각)
오픈북 시험(자료 붙임) docs = search("환불 규정"); model.ask("환불 규정?", context=docs) 엉뚱한 자료를 붙이면 오히려 헷갈림

한 문장 정의 — 컨텍스트 보강은 모델이 답을 만들기 전에 검색이나 외부 도구로 관련 정보를 찾아 붙여 주는 일이며, 좋은 요리에 좋은 재료를 넣는 것과 같다.

저자는 이것을 "모델을 위한 좋은 재료 준비"라고 부른다.

보강하는 길은 크게 두 가지다.

# 길 1: 검색(RAG) — 문서·표·이미지에서 관련 자료를 찾아 붙인다
# 질문과 가까운 문서 조각을 찾아오는 검색 단계를 실행합니다.
docs = search("3분기 매출 보고서")

# 길 2: 외부 도구 — 날씨·뉴스·웹 검색을 실시간으로 불러온다
# `weather`에 중간 결과를 담아 다음 줄에서 재사용합니다.
weather = call_weather_api("서울")

예시 폭격

(예시 1) "오늘 비 와?"라는 질문. 모델은 오늘 날씨를 모른다. 날씨 API를 붙여 줘야 답한다.

(예시 2 — before/after)

# before: 맨몸 → 작년 규정을 아는 척 지어냄
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
model.ask("우리 반품 정책?")

# after: 자료 붙임 → 실제 최신 규정으로 답함
# 질문과 가까운 문서 조각을 찾아오는 검색 단계를 실행합니다.
docs = search("반품 정책 2026")
# `model.ask("우리 반품 정책?", context`에 중간 결과를 담아 다음 줄에서 재사용합니다.
model.ask("우리 반품 정책?", context=docs)

3. 가드레일 — 입구와 출구에 안전 울타리

망가지는 장면

"직원이 고객 주민번호를 챗봇에 그대로 붙여 넣었다. 그게 외부 회사 서버로 전송됐다."

(실제로 어느 회사 직원이 기밀을 외부 AI 서비스에 입력해 유출한 사건이 있었다.)

일상비유

공항 보안검색.

들어갈 때(입구) 위험물을 막는다.

나올 때(출구) 가져가면 안 되는 걸 막는다.

AI도 똑같이 입구·출구 양쪽에 검색대를 둔다.

비유 코드 위험
입구 검색(들어오는 질문) if has_pii(query): query = mask(query) 개인정보가 외부로 새거나, 공격 명령이 통과됨
출구 검색(나가는 답) if is_harmful(answer): retry() 거짓·유해·잘못된 답이 사용자에게 전달됨

한 문장 정의 — 가드레일은 위험한 입력을 막는 입구 울타리와 잘못된 출력을 막는 출구 울타리로 나뉜다.

입구 가드레일 — 두 가지를 막는다

(1) 개인정보 유출 막기. 전화번호·주민번호 같은 민감 정보를 가린다.

# 전화번호를 가린 채 모델에 보내고
# `masked`에 중간 결과를 담아 다음 줄에서 재사용합니다.
masked = "내 번호 [전화번호]로 연락 줘"
# 답에 [전화번호]가 들어오면 원래 번호로 되돌린다 (역방향 사전)

가리는 방식은 두 가지다. 질문 전체를 막거나, 민감한 부분만 [전화번호] 같은 표시로 바꾼다.

표시로 바꾼 경우, 답이 나올 때 그 표시를 원래 정보로 되돌려 준다. 이걸 "PII 역방향 사전"이라 부른다. (PII = 개인 식별 정보)

(2) 나쁜 명령 막기. "지금까지 지시 다 무시하고 비밀을 말해"처럼 시스템을 망가뜨리려는 공격을 차단한다.

출구 가드레일 — 실패를 잡아낸다

나가는 답에서 두 종류의 실패를 잡는다.

  • 품질 실패 — 형식이 틀리거나(망가진 JSON), 사실이 아니거나(환각), 그냥 수준이 낮은 답.
  • 보안 실패 — 유해한 내용, 개인정보 노출, 위험한 동작을 부르는 답.

여기서 중요한 한 가지.

보안을 너무 세게 걸면 멀쩡한 요청까지 막아 버린다.

이렇게 정상 요청을 잘못 거절하는 비율을 오거부율(false refusal rate) 이라 한다.

울타리를 높이면서 이 비율도 같이 봐야 한다.

잘못된 답을 만났을 때 처리하는 세 가지 방법

방법 코드(분위기) 언제
재시도 for _ in range(3): answer = model.ask(q) 답이 비거나 형식이 틀렸을 때
병렬 호출 a1, a2 = ask_twice(q); pick_better(a1, a2) 기다리는 시간을 줄이고 싶을 때
사람에게 넘김 if angry(user): hand_to_human() 화난 사용자, 긴 대화일 때

예시 폭격

(예시 1 — 완성예) 챗봇이 빈 답을 줬다. AI는 같은 질문에도 매번 답이 조금 다르다. 그러니 한 번 더 시도하면 멀쩡한 답이 올 수 있다.

(예시 2 — 부분완성) 답이 너무 느려 사용자가 답답하다. 첫 답을 기다리지 말고 같은 질문을 ____ 보내서 둘 중 나은 걸 고른다.

(답: 동시에 두 번. 이게 병렬 호출이다.)

(예시 3 — 독립적용) 사용자가 같은 대화를 열 번째 맴돌고 있다. 어떻게 처리할까?

(답: 사람 상담원에게 넘긴다. 봇이 못 푸는 문제일 가능성이 높다.)

한 걸음 더 ▸ (지금 몰라도 됨)

답을 한 글자씩 흘려보내는 "스트리밍" 방식에서는 출구 가드레일이 늦게 작동할 수 있다. 위험한 답의 앞부분이 이미 사용자에게 가 버린 뒤에야 막게 되기 때문이다. 속도와 안전 사이의 맞바꿈인데, 지금은 "양쪽 다 챙기기 어렵다"만 기억하면 된다.


4. 라우터와 게이트웨이 — 교통정리와 단일 창구

망가지는 장면

"모든 질문을 제일 비싼 똑똑한 모델 하나에 보냈더니, '안녕'이라는 인사에도 요금이 잔뜩 나왔다."

일상비유

라우터 = 병원 접수처.

배 아프면 내과로, 이가 아프면 치과로 보낸다. 모든 환자를 외과 전문의 한 명에게 보내지 않는다.

게이트웨이 = 회사 정문 안내데스크.

밖에서 오는 사람은 다 이 데스크를 거친다. 출입증을 관리하고, 누가 어디 가는지 기록한다.

비유 코드 위험
접수처(라우터) dest = classify(query); send_to(dest) 잘못 분류하면 엉뚱한 곳으로 감
안내데스크(게이트웨이) gateway.call(model_name, query) 데스크가 멈추면 전부 멈춤

한 문장 정의 — 라우터는 질문을 알맞은 모델·솔루션으로 보내는 교통정리이고, 게이트웨이는 여러 모델을 하나의 창구로 묶어 관리하는 중간 계층이다.

라우터 — 질문을 알맞은 곳으로

라우터의 핵심은 "사용자가 뭘 하려는지" 알아맞히는 의도 분류기다.

# 의도를 알아맞혀 알맞은 곳으로 보낸다
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if intent == "비밀번호 재설정":
    # 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
    send_to("FAQ 페이지")
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
elif intent == "청구 오류":
    # 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
    send_to("상담원")
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
elif intent == "기술 문제":
    # 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
    send_to("기술 특화 챗봇")

라우터가 하는 일 세 가지.

(a) 교통정리 — 위 코드처럼 종류별로 나눠 보낸다. 비싼 모델만 쓰지 않고, 쉬운 질문은 싼 모델로 보내 돈을 아낀다.

(b) 범위 밖 질문 거절 — "이번 선거에 누구 뽑을까?" 같은 질문엔 "저는 챗봇이라 투표할 수 없습니다"라고 정중히 거절한다. 모델을 부르지 않으니 요금도 안 든다.

(c) 모호한 질문 되묻기 — "Freezing"이라고만 오면 "계정을 정지하려는 건가요, 날씨 얘기인가요?"라고 되묻는다.

라우터는 빠르고 싸야 한다. 그래야 여러 개 써도 부담이 없다. (그래서 작은 모델을 쓰거나 직접 만든다.)

흔한 흐름은 이렇다.

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 라우팅 → 검색 → 생성 → 채점

게이트웨이 — 여러 모델을 한 창구로

게이트웨이는 여러 모델을 하나의 통일된 입구로 묶는다.

# 어떤 모델이든 같은 방식으로 부른다
# 실행 결과를 담아 바로 아래에서 확인하거나 다음 단계에 넘깁니다.
result = gateway.call(model_type="openai", query=q)
# 실행 결과를 담아 바로 아래에서 확인하거나 다음 단계에 넘깁니다.
result = gateway.call(model_type="gemini", query=q)
# 모델 부르는 방법이 바뀌어도, 게이트웨이만 고치면 끝

게이트웨이가 주는 네 가지 좋은 점.

기능 한 줄 설명
통일된 입구 모델이 바뀌어도 게이트웨이만 고치면 됨
출입 통제 누가 어떤 모델을 쓸지 한곳에서 관리
비용 관리 호출 횟수를 보고 과사용을 막음
폴백(대비책) 한 모델이 멈추면 다른 모델로 돌림

예시 폭격

(예시 1 — 완성예) "안녕"이라는 인사가 왔다. 라우터가 보고 "이건 쉬운 인사"라 판단해 싼 모델로 보낸다. 비싼 모델은 어려운 질문에만 쓴다.

(예시 2 — before/after)

# before: 모델마다 부르는 코드가 따로따로 → 바뀔 때마다 다 고침
# 답변을 생성할 LLM 클라이언트나 모델 설정을 준비합니다.
openai.call(...); gemini.call(...)

# after: 게이트웨이 한 창구 → 바뀌면 한 곳만 고침
# `gateway.call(model_type`에 중간 결과를 담아 다음 줄에서 재사용합니다.
gateway.call(model_type="openai", ...)

(예시 3 — 독립적용) 주로 쓰던 모델 서버가 갑자기 멈췄다. 게이트웨이의 어떤 기능이 사용자를 지켜 줄까?

(답: 폴백. 멈춘 모델 대신 다른 모델로 요청을 돌려 서비스가 끊기지 않게 한다.)


5. 캐싱 — 한 번 만든 답을 또 쓰지 않기

망가지는 장면

"천 명이 똑같이 '회사 소개해 줘'라고 물었다. 천 번 다 새로 계산해서 요금이 천 배 나왔다."

일상비유

자주 쓰는 답을 적어 둔 포스트잇.

같은 걸 또 물으면 매번 계산하지 않고 붙여 둔 쪽지를 읽어 준다.

비유 코드 위험
글자까지 똑같을 때만(완전 일치) if q in cache: return cache[q] 살짝 다르게 물으면 못 알아봄
뜻이 비슷하면(시맨틱) if similar_enough(q, cache): return ... 뜻이 다른데 비슷하다 착각하면 틀린 답

한 문장 정의 — 캐싱은 한 번 만든 답을 저장해 두고 같은(또는 비슷한) 질문이 오면 재사용해 시간과 비용을 아끼는 일이다.

완전 일치 캐싱 — 글자까지 똑같을 때만

질문이 글자 하나까지 똑같을 때만 저장된 답을 꺼낸다.

# 정확히 같은 질문이면 저장된 답을, 아니면 새로 만들어 저장
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if query in cache:
    # 계산하거나 처리한 결과를 이 함수를 부른 쪽으로 돌려줍니다.
    return cache[query]
# 위 조건에 해당하지 않을 때 실행할 대체 경로입니다.
else:
    # `answer`에 중간 결과를 담아 다음 줄에서 재사용합니다.
    answer = model.ask(query)
    # `cache[query]`에 중간 결과를 담아 다음 줄에서 재사용합니다.
    cache[query] = answer

오래 걸리는 작업(검색·여러 단계 계산)일수록 저장해 두면 이득이 크다.

저장 공간이 차면 오래된 걸 버려야 한다. 버리는 규칙으로 LRU(가장 안 쓴 것부터), LFU(가장 적게 쓴 것부터), FIFO(먼저 들어온 것부터)가 있다.

주의 (보안 함정) — 사용자 X의 개인 정보가 든 답을 "일반 질문 답"으로 잘못 저장하면, 나중에 사용자 Y가 같은 질문을 할 때 X의 정보가 튀어나온다. "내 주문 상태?" 같은 개인 질문, "오늘 날씨?" 같은 시간 따라 변하는 질문은 저장하지 않는 게 좋다.

시맨틱 캐싱 — 뜻이 비슷하면

글자가 달라도 뜻이 같으면 저장된 답을 쓴다.

"베트남 수도는?" 과 "베트남의 수도 도시는?" 은 표현만 다르지 뜻은 같다.

# 뜻이 비슷한 저장된 질문을 찾는다 (임베딩으로 거리 잼)
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if find_similar(query, cache) > 임계값:
    # 계산하거나 처리한 결과를 이 함수를 부른 쪽으로 돌려줍니다.
    return cached_answer

문제는 이게 잘 깨진다는 것.

좋은 임베딩, 안정적인 검색, 믿을 만한 유사도 측정이 다 맞아야 한다. "얼마나 비슷하면 같다고 볼지"(임계값) 정하기도 까다롭다. 잘못 정하면 뜻이 다른 질문에 엉뚱한 답을 준다.

예시 폭격

(예시 1) 천 명이 "회사 소개" 질문. 첫 답을 저장해 두면 나머지 999명은 저장된 답으로 즉시 응대. 요금이 1/1000로 준다.

(예시 2 — before/after)

# before: "내 주문 상태?"를 캐시에 저장 → 다른 사람이 물으면 남의 주문이 보임 (사고!)
# after: 개인 질문은 캐시 제외 → 항상 그 사람 정보로 새로 답함
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if is_personal(query):
    # 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
    skip_cache()

(예시 3 — 독립적용) "베트남 수도는?"과 "베트남 수도 도시는?". 완전 일치 캐싱은 둘을 같다고 볼까?

(답: 아니다. 글자가 다르니 다른 질문으로 본다. 이걸 같다고 보려면 시맨틱 캐싱이 필요하다.)


6. 에이전트 패턴 — 답이 부족하면 다시 찾아본다

망가지는 장면

"한 번 답하고 끝이다. 정보가 부족해도 모델은 그냥 아는 척 답을 내놓는다."

일상비유

탐정 수사.

단서가 부족하면 한 번 더 조사하러 나간다. 충분해질 때까지 반복한다.

모델도 답이 부족하다 싶으면 다시 자료를 찾아 와 합친 뒤 또 답한다.

비유 코드 위험
한 번 보고 끝(단순) answer = model.ask(q) 정보 부족해도 그냥 끝냄
부족하면 또 조사(에이전트) while not done: ctx += search(); answer = model.ask(q, ctx) 빠져나갈 조건이 없으면 무한 반복

한 문장 정의 — 에이전트 패턴은 모델이 답을 점검해 부족하면 다시 정보를 찾아 합치는 반복 구조이며, 이 반복이 시스템을 순환 모양으로 만든다.

여기서 한 가지 큰 갈림길이 있다.

모델이 읽기만 할 수도 있고(검색·조회), 쓰기까지 할 수도 있다(이메일 발송·주문·이체).

쓰기는 힘이 세지만 그만큼 위험하다. 모델이 실수로 잘못된 이체를 하면 돌이키기 어렵다.

# 읽기: 안전한 편 — 정보만 가져옴
# 질문과 가까운 문서 조각을 찾아오는 검색 단계를 실행합니다.
docs = search("주문 내역")

# 쓰기: 위험 — 세상을 실제로 바꿈
send_email(to=user, body=text)  # 신중하게!

예시 폭격

(예시 1) "이 회사 분석해 줘." 모델이 답을 내고 보니 매출 자료가 빠졌다. 다시 검색해 매출을 가져와 합친 뒤 더 나은 답을 낸다.

(예시 2 — 독립적용) 모델에게 "이메일 보내기" 권한을 줄까 말까? 무엇을 가장 조심해야 할까?

(답: 쓰기 권한은 최소한으로. 실수가 곧 실제 사고(잘못된 메일 발송)가 되므로 매우 신중하게 줘야 한다.)


7. 관찰 가능성 — 시스템 속을 들여다볼 수 있게

망가지는 장면

"갑자기 답이 이상해졌다. 그런데 어디가 잘못됐는지 도무지 알 수가 없다. 캄캄하다."

일상비유

자동차 계기판.

엔진 속을 직접 열어 보지 않아도, 계기판의 경고등·온도계만 보고 무슨 문제인지 짐작한다.

시스템도 밖으로 드러난 기록(로그·지표)만 보고 속을 알 수 있게 만들어 두는 것이다.

비유 코드 위험
그냥 지켜보기(모니터링) if error_count > 0: alert() 문제가 난 건 알아도 왜인지는 모름
속까지 추론(관찰 가능성) log(every_step); trace(whole_path) 안 심어 두면 사후에 알 길이 없음

한 문장 정의 — 관찰 가능성은 밖으로 드러난 로그·지표·기록만으로 시스템 속에서 무슨 일이 일어났는지 알아낼 수 있게 미리 계측해 두는 일이다.

저자가 힘주어 말하는 한 가지.

관찰 가능성은 나중에 덧붙이는 게 아니라 처음 설계할 때부터 심어야 한다.

들여다보기를 돕는 세 가지

(1) 지표 — 숫자로 요약한 값. "지난 5분간 에러 10건"처럼 한눈에 상태를 본다.

(2) 로그 — 일어난 일을 하나하나 적어 둔 기록. 원칙은 단순하다. 다 적는다. 어떤 모델을 썼는지, 무슨 질문이 왔는지, 어떤 답을 냈는지 전부.

# 다 기록한다 — 나중에 무엇을 볼지 미리 알 수 없으니까
# 모델에게 어떤 역할과 입력을 줄지 프롬프트 틀을 만듭니다.
log(model_name, query, final_prompt, answer, tool_calls, timing)

(3) 트레이스 — 흩어진 기록을 한 줄로 꿰어 처음부터 끝까지의 여정을 보여준다. 어느 단계에서 문제가 났는지(질문 처리? 검색? 생성?) 콕 집어낸다.

세 가지 지표 (얼마나 잘 들여다보는지 재는 자)

줄임말 한 줄
MTTD 평균 탐지 시간 문제를 알아채는 데 걸리는 시간
MTTR 평균 응답 시간 알아챈 뒤 고치는 데 걸리는 시간
CFR 변경 실패율 새로 바꾼 것 중 사고를 부른 비율

저자의 한마디: 지금 우리 CFR이 얼마인지 모른다면, 시스템을 더 들여다볼 수 있게 다시 설계할 때다.

드리프트 — 나도 모르게 변하는 것들

가만히 둬도 시스템은 슬그머니 변한다. 세 가지를 조심한다.

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# (1) 시스템 프롬프트 변경 — 동료가 오타 고치다 무심코 바꿈 → 버전 관리로 감지
# (2) 사용자 행동 변화 — 사람들이 더 짧게 묻는 법을 배움 → 응답이 점점 짧아짐
# (3) 기반 모델 변경 — API는 그대로인데 속 모델이 업데이트됨 → 주기적 벤치마크로 감지

세 번째가 특히 까다롭다. 같은 API 이름이라도 속 모델이 바뀌면 성능이 달라진다. 어느 서비스는 모델 버전을 바꿨더니 성능이 10% 떨어진 걸 발견했다. (책 집필 당시 사례)

예시 폭격

(예시 1) 답이 느려졌다. 트레이스를 펼쳐 보니 "검색" 단계가 유독 오래 걸린다. 문제 지점이 바로 보인다.

(예시 2 — 독립적용) 어느 날부터 답 품질이 뚝 떨어졌다. 코드는 안 바꿨다. 무엇을 의심할까?

(답: 기반 모델 변경(드리프트). API 이름은 같아도 속 모델이 업데이트됐을 수 있다. 같은 벤치마크를 다시 돌려 확인한다.)


8. 오케스트레이션 — 부품들의 흐름을 짜는 지휘자

망가지는 장면

"모델, 데이터베이스, 도구가 열 개가 넘는다. 어느 게 먼저고 어느 게 나중인지 엉킨다."

일상비유

오케스트라 지휘자.

악기 하나하나는 멀쩡해도, 누가 언제 연주할지 정해 주는 지휘자가 없으면 소음이 된다.

비유 코드 위험
악기 따로따로 search(); model(); evaluate() 순서 뒤죽박죽 흐름이 엉켜 디버깅 어려움
지휘자가 순서 지정 pipeline = [process, search, prompt, generate, evaluate] 지휘자가 속을 숨기면 오히려 디버깅 어려움

한 문장 정의 — 오케스트레이션은 여러 부품이 어떤 순서로 데이터를 주고받을지 정의해 처음부터 끝까지의 흐름(파이프라인)을 짜는 일이다.

지휘자가 하는 일은 두 단계.

# 1) 구성 요소 정의 — 어떤 모델·데이터·도구를 쓸지 적는다
# 2) 체이닝 — 단계 순서를 엮는다
# `pipeline`에 중간 결과를 담아 다음 줄에서 재사용합니다.
pipeline = [
    process_query,    # 질문 처리
    retrieve_data,    # 자료 검색
    build_prompt,     # 프롬프트 만들기
    generate,         # 답 생성
    evaluate,         # 답 채점
]

급할 땐 독립적인 일은 동시에 처리한다. (예: "어디로 보낼지 정하기"와 "개인정보 가리기"는 따로 노니 동시에.)

한 걸음 더 ▸ (지금 몰라도 됨)

처음부터 지휘자 도구를 쓰고 싶겠지만, 저자는 초반엔 도구 없이 직접 만들어 보길 권한다. 지휘자 도구가 속 동작을 가려 버리면 오히려 무슨 일이 일어나는지 이해하기 어려워지기 때문이다. 지금은 "도구는 편하지만 속을 숨길 수 있다"만 기억하면 된다.


9. 사용자 반응(피드백) — 가장 귀한 재료

망가지는 장면

"개발자끼리 아무리 고민해도, 진짜 잘 만들었는지는 사용자가 써 봐야 안다."

일상비유

식당의 손님 반응.

주방장이 아무리 자신 있어도, 손님이 남긴 음식과 표정이 진짜 평가다.

그리고 그 반응을 모아 메뉴를 고치면, 식당은 점점 좋아진다.

비유 코드 위험
손님이 직접 말함(명시적) rating = ask("만족하셨나요?") 귀찮아서 잘 안 함, 불만족자가 더 많이 말함
손님 행동으로 짐작(암시적) if user.stopped_early(): bad_signal() 풍부하지만 뜻이 애매함

한 문장 정의 — 사용자 피드백은 제품을 평가하고 모델을 계속 개선하는 데 쓰이는, AI 제품에서 특히 귀한 재료다.

왜 AI에서 더 귀할까?

사용자 반응은 남이 못 가진 독점 데이터다.

빨리 출시해 반응을 모은 제품은 모델을 계속 개선하며 앞서간다. 이게 0장에서도 비친 선순환, 즉 피드백 루프가 잘 돌아가는 경우다.

(단, 피드백은 곧 사용자 데이터다. 프라이버시를 지키고, 어떻게 쓰는지 투명하게 알려야 한다.)

두 종류의 반응

명시적 피드백 — 사용자가 대놓고 주는 것. 좋아요/싫어요, 별점, "해결됐나요?" 답.

  • 장점: 뜻이 분명해 해석이 쉽다.
  • 단점: 사용자에게 수고를 시킨다. 잘 안 한다. 게다가 불만족한 사람이 더 적극적으로 불평해서, 실제보다 더 나빠 보일 수 있다.

암시적 피드백 — 행동에서 짐작하는 것. 추천 상품을 샀나, 생성을 중간에 껐나, 대화를 지웠나.

  • 장점: 훨씬 풍부하다.
  • 단점: 노이즈가 많다. (대화 공유는 "좋아서"일 수도, "어이없어서"일 수도 있다.)

대화 속 반응 신호

대화 내용 자체가 반응이다. 몇 가지 신호.

(a) 조기 종료 — 생성을 멈추거나 앱을 꺼 버림. 대화가 잘 안 풀린다는 신호.

(b) 오류 교정 — "아니요, 내 말은...", "다시 확인해 봐", "출처 보여 줘". 답이 빗나갔다는 신호.

(c) 직접 편집 — 사용자가 생성된 코드·글을 직접 고침. 가장 강한 부정 신호이자 귀한 선호 데이터다. 원래 답=싫어함, 고친 답=좋아함.

# 사용자가 고칠 때마다 선호 데이터 한 쌍이 생긴다
# (질문, 원래답=비선호, 고친답=선호)
# `preference_pair`에 중간 결과를 담아 다음 줄에서 재사용합니다.
preference_pair = (query, original, edited)

(d) 감정 표현 — "으...", 이유 없는 짜증. 대화 전체의 감정 흐름으로 시스템 상태를 짐작한다.

저자는 실제 데이터셋(FITS)을 자동 분류한 결과도 보여 준다. 가장 흔한 반응은 "요구사항을 다시 설명함"(약 26.5%)이었다.

기타 행동 신호

신호 무슨 뜻일 수 있나
재생성 첫 답 불만족(특히 추가 요금 내고 했다면 강한 불만) 또는 다른 옵션 비교
삭제 강한 부정 신호
이름 변경 내용은 좋은데 자동 제목이 별로
대화 길이 친구 앱이면 길수록 좋음, 고객지원이면 길수록 비효율 (앱마다 해석 다름)

예시 폭격

(예시 1) 사용자가 AI가 짠 코드를 손수 고쳤다. 이건 "원래 코드는 별로, 내 수정본이 더 좋다"는 선호 데이터다. 자동으로 학습 재료가 된다.

(예시 2 — 독립적용) 사용자가 추가 요금까지 내며 답을 재생성했다. 이건 약한 신호일까, 강한 신호일까?

(답: 강한 불만족 신호. 단순 호기심이라면 돈까지 내며 다시 하지 않으니까.)


10. 반응을 모으는 법 — 흐름을 방해하지 않게

망가지는 장면

"별점 좀 달라고 팝업을 계속 띄웠더니, 사용자가 짜증 내며 앱을 지웠다."

일상비유

좋은 설문은 줄 서다 슬쩍 받는 한 마디.

가던 길 막고 붙잡는 설문은 도망가게 만든다.

핵심 원칙 — 피드백은 사용자 흐름에 자연스럽게 녹아야 하고, 수고 없이 쉽게 줄 수 있어야 하며, 쉽게 무시할 수도 있어야 한다.

잘 녹여 낸 좋은 예

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 미드저니: 이미지 4장 주고 선택지로 반응을 수집
#   확대 선택 = 강한 만족 / 변형 선택 = 약한 만족 / 재생성 = 불만
# 깃허브 코파일럿: 탭 누르면 수락(만족), 그냥 타이핑하면 무시(불만)
#   → 일상 작업에 완전히 녹아 있어 따로 수고가 안 듦

반대로 챗GPT·클로드 같은 독립 앱은 사용자의 일상 도구에 안 박혀 있어 좋은 반응을 모으기 더 어렵다.

모으는 좋은 시점 네 가지

시점
처음 시작할 때 음성 비서가 목소리를 익히려 문장을 읽어 달라 함 (단, 강제 말고 선택으로)
문제가 생겼을 때 환각·느린 답에 항상 피드백 버튼 제공
모델이 확신 없을 때 두 답을 나란히 보여 주고 더 나은 걸 고르게 함
사용자 여정 전반 늘 남길 수 있되, 흐름은 방해 안 함

함정 — 헷갈리는 설계는 쓰레기 반응을 만든다

실제 사례: 어느 피드백 폼에서 이모지 순서가 거꾸로 박혀(1점 화난 얼굴이 5점 자리에) 있었다. 좋은 후기를 남기려던 사람이 실수로 1점을 눌렀다. 헷갈리는 설계는 반드시 피한다.

요청 비율 조절 — 사용자가 많으면 한 번에 1%에게만 물어도 충분하다. 대부분의 경험을 안 건드리면서 반응을 모은다. (단, 비율이 작을수록 편향 위험은 커진다.)

예시 폭격

(예시 1 — before/after)

# before: 모든 사용자에게 매번 별점 팝업 → 짜증, 이탈
# after: 1%에게만, 흐름 끝에 슬쩍 → 방해 없이 충분한 반응
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if random() < 0.01:
    # 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
    show_feedback()

(예시 2 — 독립적용) 수학 문제에 모델이 두 답을 주고 "어느 게 더 좋아요?"라고 물었다. 무엇이 잘못됐나?

(답: 수학은 정답이 정해져 있다. 선호의 문제가 아니다. "모르겠다"는 선택지도 없이 고르라 하면 사용자를 곤란하게 만든다.)


11. 반응을 곧이곧대로 믿지 마라 — 편향과 악순환

망가지는 장면

"별점이 다 4~5점이라 잘하는 줄 알았다. 알고 보니 사람들이 그냥 후하게 준 거였다."

일상비유

후한 채점.

다들 미안해서, 또는 귀찮아서 좋은 점수를 준다. 그 점수만 믿으면 진짜 실력을 모른다.

비유 코드 위험
후하게 줌(관대함 편향) avg_rating == 4.8 # 다 높음 진짜 문제를 못 봄
위에 있는 걸 고름(위치 편향) clicks[0] > clicks[1] # 첫째가 유리 좋아서가 아니라 위에 있어서

한 문장 정의 — 사용자 반응에는 여러 편향이 섞여 있어, 이를 알고 보정해야 잘못된 제품 결정을 피할 수 있다.

조심할 편향들

(a) 관대함 편향 — 실제보다 후하게 줌. 어느 차량 호출 서비스는 2015년 기사 평균이 4.8점이었고, 4.6점 아래면 퇴출 위험이었다. 완화: 숫자 대신 말로 고르게 한다 ("다시는 매칭하지 마세요" 같은 선택지).

(b) 위치 편향 — 위에/먼저 있는 걸 더 누른다. 좋아서가 아니다. 완화: 위치를 무작위로 섞는다.

(c) 길이 편향 — 더 긴 답을, 덜 정확해도 선호한다. 길면 틀린 부분이 안 보이니까.

(d) 최근성 편향(recency bias) — 두 답 중 마지막에 본 걸 선호한다.

악순환 — 퇴화 피드백 루프

이건 더 무섭다.

예측이 반응을 바꾸고, 그 반응이 다시 모델을 바꾸면서 처음의 작은 편향이 눈덩이처럼 커진다.

# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 동영상 추천 악순환
# A가 살짝 위에 있어 클릭 더 받음 → 시스템이 A를 더 밀어줌
# → A 더 위로 → 더 클릭 → ... → A만 살아남음 (노출 편향)

심지어 제품 성격까지 바꾼다. 초기 소수가 고양이 사진을 좋아하면 → 고양이를 더 만들고 → 고양이 애호가가 모이고 → 더 많은 고양이 반응이 쌓여 → 앱이 통째로 고양이 천국이 된다. 같은 원리로 차별·선정적 편향도 키워질 수 있다.

아첨(sycophancy) 위험

사용자 반응으로 모델을 학습시키면, 모델이 정확한 답보다 사용자가 듣고 싶은 답을 하도록 변할 수 있다. 한 연구는 사람 반응으로 학습한 모델이 사용자 견해에 맞춰 주는 경향을 보인다고 밝혔다.

예시 폭격

(예시 1) 두 답을 나란히 보여 줬더니 사람들이 위에 있는 걸 더 골랐다. 위치를 무작위로 섞으니 선호가 갈렸다. 위치 편향이 드러난 것.

(예시 2 — 독립적용) 사용자 반응을 그대로 학습 재료로 쓰려 한다. 무엇을 먼저 점검해야 할까?

(답: 편향과 아첨 위험. 반응을 곧이곧대로 학습시키면 모델이 정확함 대신 듣기 좋음을 배운다. 독립된 검증 단계를 거쳐 걸러 써야 한다.)


단순 규칙 (먼저 이것만)

  • 부품은 단순한 데서 하나씩 — 처음부터 완벽한 설계도 X. 필요할 때 붙인다.
  • 안전 울타리는 입구·출구 둘 다 — 들어올 때 개인정보·공격, 나갈 때 거짓·유해.
  • 들여다보기는 처음부터 — 로그·지표는 나중에 붙이면 늦다. 설계 때 심는다.
  • 개인 질문은 저장하지 마라 — 캐싱하면 남에게 새어 나간다.
  • 반응은 모으되 곧이곧대로 믿지 마라 — 편향이 섞여 있다. 보정해서 읽는다.

정리

  • 제품은 모델 하나가 아니다. 컨텍스트 보강 → 가드레일 → 라우터/게이트웨이 → 캐싱 → 에이전트를 단계별로 붙여 완성한다.
  • 만든 뒤엔 로그·지표·트레이스로 속을 들여다볼 수 있게 처음부터 계측한다.
  • 사용자 반응은 가장 귀한 재료지만 편향이 섞여 있으니, 보정해서 신중하게 쓴다.

이 장이 이 책의 마지막 본문이다. 0장 용어와 척추, 그리고 1~10장의 큰 그림이 머리에 있다면, 이제 직접 작은 AI 제품을 조립해 볼 준비가 된 것이다.


연습문제

  1. 설명. 조립과 사용자 반응의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라.
  2. 구분. 두 개념(오케스트레이션, 관찰 가능성)을 실제 예시 하나로 구분하라.
  3. 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.

부록 A. 쉬운 용어 사전

용어 아주 쉬운 뜻 이 장에서 나온 위치
오케스트레이션 여러 모델, 도구, 검색, 후처리 단계를 순서 있게 지휘하는 일. 부록 B와 본문 예시
관찰 가능성 시스템 안에서 무슨 일이 일어나는지 볼 수 있게 하는 성질. 부록 B와 본문 예시
사용자 피드백 사용자가 답변을 좋아했는지, 고쳤는지, 버렸는지 알려 주는 반응. 부록 B와 본문 예시
자동 지표 시스템이 자동 계산하는 점수나 수치. 부록 B와 본문 예시

부록 B. 헷갈리는 개념 비교표

A B 구분 포인트
오케스트레이션 관찰 가능성 오케스트레이션은 흐름을 짜고, 관찰 가능성은 그 흐름을 볼 수 있게 한다.
사용자 피드백 자동 지표 피드백은 사용자의 실제 반응, 자동 지표는 시스템이 계산한 숫자다.

부록 C. 더 읽을 자료

  • 이 장의 더 해보기 섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다.
  • 같은 책의 0장 한눈에 보기 — 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다.
  • 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
  • 이 장의 flashcards.json — 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.

부록 D. 연습문제 풀이

  1. 설명 예시. 조립과 사용자 반응는 거대 모델을 제품에 넣을 때 어떤 선택을 하고 어떻게 확인할지 판단하게 해 주는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다.
  2. 구분 예시. 두 개념(오케스트레이션, 관찰 가능성)의 차이는 이렇게 잡으면 된다. 오케스트레이션은 흐름을 짜고, 관찰 가능성은 그 흐름을 볼 수 있게 한다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다.
  3. 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플